import React, { cloneElement, ComponentType, createElement, ReactNode, useMemo } from 'react';
import { isValidElementType, isElement } from 'react-is';

export type ErrorLike = Error | { message: string } | string;

export type WithErrorProps = {
  error: ErrorLike
}

export type ErrorDisplayProps = {
  children?: ReactNode | ReactNode[],
} & WithErrorProps

let DefaultErrorDisplay: ComponentType<WithErrorProps> | null = null;

export const setDefaultErrorDisplay = (component?: ComponentType<WithErrorProps> | null) => {
  DefaultErrorDisplay = component || null;
};

export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ error }) => {
  if (DefaultErrorDisplay != null) {
    return createElement(DefaultErrorDisplay, { error });
  }

  return (
    <div>
      An error occurred: <ErrorMessage error={error}/>
    </div>
  );
};

export type ErrorMessageProps = {
  error?: ErrorLike | null,
  unknownText?: string,
}

export const ErrorMessage: React.FC<ErrorMessageProps> = ({
  error,
  unknownText = 'Unknown reason',
}) => {
  const message = useMemo(() => {
    if (error != null) {
      switch (typeof error) {
        case 'string' :
          return error || unknownText;
        case 'object' :
          return error.message || unknownText;
      }
    }
    return unknownText;
  }, [error, unknownText]);

  return <>{message}</>;
};

export type ErrorFallbackComponent =
  React.ReactElement<WithErrorProps>
  | React.ComponentType<WithErrorProps>
  | undefined
  | null;

export type ErrorFallbackProps = {
  error?: ErrorLike,
  fallback?: ErrorFallbackComponent,
  shouldFallback?: boolean | ((error: ErrorLike) => boolean),
  children?: ReactNode | ReactNode[],
}

export const ErrorFallback: React.FC<ErrorFallbackProps> = ({
  error,
  fallback,
  shouldFallback = true,
  children,
}) => {
  const hasError = error != null;
  const shouldRenderFallback = useMemo(() => {
    if (hasError && shouldFallback) {
      if (typeof shouldFallback === 'function') {
        return shouldFallback(error);
      }
      return true;
    }
    return false;
  }, [hasError, shouldFallback]);

  if (shouldRenderFallback) {
    return isElement(fallback) ? cloneElement(fallback, { error }) : (
      isValidElementType(fallback) ? createElement(fallback, { error }) : (
        <ErrorDisplay error={error as ErrorLike}/>
      )
    );
  }
  return <>{children}</>;
};
